package f.h.dragon.patterns.money;

import java.util.*;

/**
 * MoneyBag: A MoneyBag defers exchange rate conversions. For example adding 12
 * Swiss Francs to 14 US Dollars is represented as a bag containing the two
 * Monies 12 CHF and 14 USD. Adding another 10 Swiss francs gives a bag with 22
 * CHF and 14 USD. Due to the deferred exchange rate conversion we can later
 * value a MoneyBag with different exchange rates.
 * 
 * A MoneyBag is represented as a list of Monies and provides different
 * constructors to create a MoneyBag.
 * 
 * @author Wenlong Meng(wenlong.meng@gmail.com)
 * @version 1.0 at 2012/04/21
 * @since 1.0
 */
public class MoneyBag implements IMoney {
	
	//local variables
	/**
	 * store monies
	 */
	private Vector<IMoney> fMonies = new Vector<IMoney>(5);
	
	//Logic
	/**
	 * create money bag
	 * 
	 * @param m1
	 * @param m2
	 * @return
	 */
	static IMoney create(IMoney m1, IMoney m2) {
		MoneyBag result = new MoneyBag();
		m1.appendTo(result);
		m2.appendTo(result);
		return result.simplify();
	}

	/**
	 * Adds a money to this money.
	 * 
	 * @param m
	 * @return
	 * 
	 * @see f.h.dragon.patterns.money.IMoney#add(f.h.dragon.patterns.money.IMoney)
	 */
	@Override
	public IMoney add(IMoney m) {
		return m.addMoneyBag(this);
	}

	/**
	 * Adds a simple Money to this money. This is a helper method for
	 * implementing double dispatch
	 * 
	 * @param m
	 * @return
	 * 
	 * @see f.h.dragon.patterns.money.IMoney#addMoney(f.h.dragon.patterns.money.Money)
	 */
	@Override
	public IMoney addMoney(Money m) {
		return MoneyBag.create(m, this);
	}

	/**
	 * Adds a MoneyBag to this money. This is a helper method for implementing
	 * double dispatch
	 * 
	 * @param s
	 * @return
	 * 
	 * @see f.h.dragon.patterns.money.IMoney#addMoneyBag(f.h.dragon.patterns.money.MoneyBag)
	 */
	@Override
	public IMoney addMoneyBag(MoneyBag s) {
		return MoneyBag.create(s, this);
	}

	/**
	 * append bag
	 * 
	 * @param aBag
	 */
	void appendBag(MoneyBag aBag) {
		for (Enumeration<IMoney> e = aBag.fMonies.elements(); e.hasMoreElements();)
			appendMoney((Money) e.nextElement());
	}

	/**
	 * append money
	 * 
	 * @param aMoney
	 */
	void appendMoney(Money aMoney) {
		if (aMoney.isZero())
			return;
		IMoney old = findMoney(aMoney.currency());
		if (old == null) {
			fMonies.addElement(aMoney);
			return;
		}
		fMonies.removeElement(old);
		IMoney sum = old.add(aMoney);
		if (sum.isZero())
			return;
		fMonies.addElement(sum);
	}

	/**
	 * find money from money-bag
	 * 
	 * @param currency
	 * @return
	 */
	private Money findMoney(String currency) {
		for (Enumeration<IMoney> e = fMonies.elements(); e.hasMoreElements();) {
			Money m = (Money) e.nextElement();
			if (m.currency().equals(currency))
				return m;
		}
		return null;
	}

	/**
	 * Tests whether the money-bag is contains m.
	 * 
	 * @param m
	 * @return
	 */
	private boolean contains(Money m) {
		Money found = findMoney(m.currency());
		if (found == null)
			return false;
		return found.amount() == m.amount();
	}

	/**
	 * Tests whether this money-bag is zero
	 * 
	 * @return
	 * 
	 * @see f.h.dragon.patterns.money.IMoney#isZero()
	 */
	@Override
	public boolean isZero() {
		return fMonies.size() == 0;
	}

	/**
	 * Multiplies a money by the given factor.
	 * 
	 * @param factor
	 * @return
	 * 
	 * @see f.h.dragon.patterns.money.IMoney#multiply(int)
	 */
	@Override
	public IMoney multiply(int factor) {
		MoneyBag result = new MoneyBag();
		if (factor != 0) {
			for (Enumeration<IMoney> e = fMonies.elements(); e.hasMoreElements();) {
				Money m = (Money) e.nextElement();
				result.appendMoney((Money) m.multiply(factor));
			}
		}
		return result;
	}

	/**
	 * Multiplies a money by the given factor.
	 * 
	 * @param factor
	 * @return
	 * 
	 * @see f.h.dragon.patterns.money.IMoney#negate()
	 */
	@Override
	public IMoney negate() {
		MoneyBag result = new MoneyBag();
		for (Enumeration<IMoney> e = fMonies.elements(); e.hasMoreElements();) {
			Money m = (Money) e.nextElement();
			result.appendMoney((Money) m.negate());
		}
		return result;
	}

	/**
	 * simplify
	 * 
	 * @return
	 */
	private IMoney simplify() {
		if (fMonies.size() == 1)
			return (IMoney) fMonies.elements().nextElement();
		return this;
	}

	/**
	 * Subtracts a money from this money.
	 * 
	 * @param m
	 * @return
	 * 
	 * @see f.h.dragon.patterns.money.IMoney#subtract(f.h.dragon.patterns.money.IMoney)
	 */
	@Override
	public IMoney subtract(IMoney m) {
		return add(m.negate());
	}

	/**
	 * Append this to a MoneyBag m.
	 * 
	 * @param m
	 * 
	 * @see f.h.dragon.patterns.money.IMoney#appendTo(f.h.dragon.patterns.money.MoneyBag)
	 */
	@Override
	public void appendTo(MoneyBag m) {
		m.appendBag(this);
	}
	
	//equals
	/**
	 * Tests whether tow IMoneys are equal.
	 * 
	 * @param anObject
	 * @return
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object anObject) {
		if (isZero())
			if (anObject instanceof IMoney)
				return ((IMoney) anObject).isZero();
		if (anObject instanceof MoneyBag) {
			MoneyBag aMoneyBag = (MoneyBag) anObject;
			if (aMoneyBag.fMonies.size() != fMonies.size())
				return false;
			for (Enumeration<IMoney> e = fMonies.elements(); e.hasMoreElements();) {
				Money m = (Money) e.nextElement();
				if (!aMoneyBag.contains(m))
					return false;
			}
			return true;
		}
		return false;
	}
	
	//hashCode
	/**
	 * Returns a hash code value for the object.
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		int hash = 0;
		for (Enumeration<IMoney> e = fMonies.elements(); e.hasMoreElements();) {
			Object m = e.nextElement();
			hash ^= m.hashCode();
		}
		return hash;
	}
	
	//toString
	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		StringBuffer buffer = new StringBuffer();
		buffer.append("{");
		for (Enumeration<IMoney> e = fMonies.elements(); e.hasMoreElements();)
			buffer.append(e.nextElement());
		buffer.append("}");
		return buffer.toString();
	}

}
